package com.zoro.barn.commons.redis.tools;

import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.support.ConnectionPoolSupport;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author zhaoxingwu
 */
public class RedisOperation {

    private static final String LOCK_PREFIX = "LOCK::";

    private final GenericObjectPool<StatefulRedisConnection<String, String>> pool;

    RedisOperation(RedisToolProperties redisToolProperties) {
        RedisURI.Builder builder =
                RedisURI.Builder.redis(redisToolProperties.getHost(), redisToolProperties.getPort())
                        .withTimeout(Duration.ofMillis(redisToolProperties.getTimeout()))
                        .withDatabase(redisToolProperties.getDb());
        String ps = redisToolProperties.getPassword();
        if (ps != null && ps.trim().length() > 0) {
            builder.withPassword(ps);
        }
        RedisClient redisClient = RedisClient.create(builder.build());

        GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();

        int size = redisToolProperties.getPoolSize();
        if (size <= 0) {
            size = 5;
        }
        poolConfig.setMaxTotal(size);
        long timeout = redisToolProperties.getTimeout();
        if (timeout >= 0) {
            poolConfig.setMaxWaitMillis(redisToolProperties.getTimeout());
        }
        pool = ConnectionPoolSupport
                .createGenericObjectPool(redisClient::connect, poolConfig);
    }

    private StatefulRedisConnection<String, String> connect() {
        try {
            return this.pool.borrowObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private <OUT> OUT execute(Function<RedisCommands<String, String>, OUT> function) {

        try (StatefulRedisConnection<String, String> connection = connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return function.apply(commands);
        }
    }

    /**
     * redis 锁
     *
     * @param lockId      lock id
     * @param value       value
     * @param ttl         how long this lock save in redis
     * @param waitTimeout how long you can wait for this lock
     * @return is lock success
     */
    public String lock(String lockId, String value, Duration ttl, Duration waitTimeout) {
        String id = LOCK_PREFIX + lockId;
        return execute(commands -> {
            SetArgs args = SetArgs.Builder.nx().ex(ttl.toSeconds());
            long start = System.currentTimeMillis();

            // 先取一次是因为 waitTimeout 可能是 0 ,如果是 0 那么下面的 while 就可能进不去
            String result = commands.set(id, value, args);
            if ("OK".equals(result)) {
                return null;
            } else {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            while (waitTimeout.toMillis() - (System.currentTimeMillis() - start) > 0) {
                result = commands.set(id, value, args);
                if ("OK".equals(result)) {
                    return null;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            String lockingData = get(id);
            if (lockingData == null) {
                // 这种情况发生在最后一次获取锁之后锁到期，然后再 get 的时候就会为null
                lockingData = "";
            }
            return lockingData;
        });
    }

    public void unlock(String lockId) {
        String id = LOCK_PREFIX + lockId;
        execute(commands -> {
            commands.del(id);
            return null;
        });
    }

    public String get(String key) {
        return execute(commands -> commands.get(key));
    }


}
